﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

namespace SimpleServiceBus.Tests
{
    public class EventRecorder: IDisposable
    {
        private readonly List<EventInvocation> _eventHistory = new List<EventInvocation>();
        private Dictionary<string, EventRef> _handlers;

        public EventRecorder(object eventSource)
        {
            if (eventSource == null)
                throw new ArgumentNullException("eventSource");
            EventSource = eventSource;
            BindToEventSource();
        }

        public object EventSource { get; private set; }

        public EventInvocation[] EventHistory
        {
            get { return _eventHistory.ToArray(); }
        }

        #region IDisposable Members

        public void Dispose()
        {
            UnBindFromEventSource();
        }

        #endregion

        public event EventHandler<EventRecorderEventArgs> EventRecorded;

        public int EventCount { get { return EventHistory.Length;}}
        public int EventCountFor(string eventName) {return GetInvocationsFor(eventName).Length;}

        public void ClearEventHistory()
        {
            _eventHistory.Clear();
        }

        public EventInvocation[] GetInvocationsFor(string eventName)
        {
            var output = new List<EventInvocation>();
            foreach (EventInvocation invocation in EventHistory)
            {
                if (invocation.EventName.Equals(eventName,StringComparison.OrdinalIgnoreCase))
                    output.Add(invocation);
            }
            return output.ToArray();
        }

        public bool HasInvocation(string eventName)
        {
            return GetInvocationsFor(eventName).Length > 0;
        }

        
        protected void RecordEvent(EventArgs args, string eventName)
        {
            EventInfo evt = EventSource.GetType().GetEvent(eventName);

            if (evt != null)
            {
            var history = new EventInvocation { Args = args, Event = evt };
            _eventHistory.Add(history);
            OnEventRecorded(history);
            }
        }

        protected void BindToEventSource()
        {
            _handlers = new Dictionary<string, EventRef>();
            foreach (EventInfo evt in EventSource.GetType().GetEvents())
            {
                BindToEvent(evt);
            }
        }

        protected void UnBindFromEventSource()
        {
            if (EventSource == null) return;

            foreach (EventInfo evt in EventSource.GetType().GetEvents())
            {
                UnBindFromEvent(evt);
            }

            EventSource = null;
        }

        

        protected void BindToEvent(EventInfo evt)
        {
            Type handlerType = evt.EventHandlerType;

            if (handlerType == null)
                throw new ArgumentException("No handler could be identified for " + evt.Name);

            Type[] handlerParams = GetDelegateParameterTypes(handlerType);

            //Not technically required, but may be helpful if an exception is thrown
            String dynamicHandlerName = String.Format("EventSource_{0}", evt.Name);

            var handlerDef = new DynamicMethod(dynamicHandlerName,
                                               typeof(void),
                                               handlerParams,
                                               typeof (EventRecorder));


            MethodInfo recordEvent =
                typeof (EventRecorder).GetMethod("RecordEvent", BindingFlags.Instance | BindingFlags.NonPublic);

            ILGenerator ilgen = handlerDef.GetILGenerator();
            
            //load 'this' onto the stack
            ilgen.Emit(OpCodes.Ldarg_0);

            //load the second argument (EventArgs) onto the stack
            //We're not using the sender parameterf of the delegate,
            //so we skip emitting OpCodes.Ldarg_1
            ilgen.Emit(OpCodes.Ldarg_2);

            //Load the name of the event onto the stack
            ilgen.Emit(OpCodes.Ldstr, evt.Name);

            //Call the RecordEvent method, passing the two loaded argumetns
            //EventArgs args and String name
            ilgen.Emit(OpCodes.Call, recordEvent);

            //Return control
            ilgen.Emit(OpCodes.Ret);

            //Create a delegate out of this new handler, binding to the current
            //instance as the target
            Delegate handler = handlerDef.CreateDelegate(handlerType,this);

            //Register the new delegeate with the event on the event source
            evt.AddEventHandler(EventSource, handler);

            //record this handler so we can get to it later
            //when we need to remove our registration from 
            //the event source during dispose
            _handlers.Add(evt.Name, new EventRef(handler));
        }
       
        protected void UnBindFromEvent(EventInfo evt)
        {
            if (EventSource == null || !_handlers.ContainsKey(evt.Name))
                return;


            EventRef eRef = _handlers[evt.Name];
            if (eRef != null)
            {
                evt.RemoveEventHandler(EventSource, eRef.Handler);
            }
        }

        private static Type[] GetDelegateParameterTypes(Type d)
        {
            if (d.BaseType != typeof (MulticastDelegate))
                throw new ApplicationException(d.Name + " is not a delegate.");

            MethodInfo invoke = d.GetMethod("Invoke");
            if (invoke == null)
                throw new ApplicationException(d.Name + " is not a delegate.");

            ParameterInfo[] parameters = invoke.GetParameters();
            var typeParameters = new Type[parameters.Length+1];
            for (int i = 0; i < parameters.Length; i++)
            {
                typeParameters[i+1] = parameters[i].ParameterType;
            }
            typeParameters[0] = typeof (EventRecorder);
            return typeParameters;
        }

        protected void OnEventRecorded(EventInvocation evt)
        {
            EventHandler<EventRecorderEventArgs> del = EventRecorded;
            if (del != null)
                del(this, new EventRecorderEventArgs { Event = evt });
        }

        #region Nested type: EventRef

        private class EventRef
        {
            public readonly Delegate Handler;

            public EventRef(Delegate handler)
            {
                Handler = handler;
            }
        }

        #endregion
    }

    public class EventInvocation
    {
        public EventInvocation()
        {
            Timestamp = DateTime.Now;
        }

        public EventInfo Event { get; set; }
        public EventArgs Args { get; set; }
        public DateTime Timestamp { get; set; }

        public string EventName
        {
            get { return Event != null ? Event.Name : String.Empty; }
        }
    }

    public class EventRecorderEventArgs : EventArgs
    {
        public EventInvocation Event { get; set; }
    }
}